package org.springframework.samples.countries.web.translate;

import java.util.EmptyStackException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Stack;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.objectweb.asm.Label;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.EmptyVisitor;

import pikes.ecma.ExpressionStatement;
import pikes.ecma.IExpression;
import pikes.ecma.Identifier;
import pikes.ecma.ReturnStatement;
import pikes.ecma.SourceElement;
import pikes.ecma.Statement;

public class MethodBlockTranslator extends EmptyVisitor {

	protected static final Log translatorLogger = LogFactory.getLog("Translator");

	public static Map<Integer, ByteCodeOperation> CONSTANT_BYTECODE_OPERATIONS = new HashMap<Integer, ByteCodeOperation>();

	static {
		CONSTANT_BYTECODE_OPERATIONS.put(Opcodes.ICONST_M1, new LoadMinusOne());
		CONSTANT_BYTECODE_OPERATIONS.put(Opcodes.ICONST_1, new LoadOne());
		CONSTANT_BYTECODE_OPERATIONS.put(Opcodes.ACONST_NULL, new LoadNull());
		CONSTANT_BYTECODE_OPERATIONS.put(Opcodes.RETURN, new ReturnOperation());
		CONSTANT_BYTECODE_OPERATIONS.put(Opcodes.ARETURN, new AReturnOperation());
		CONSTANT_BYTECODE_OPERATIONS.put(Opcodes.DUP, new DuplicateOperation());
	}

	private String name = null;

	private int numberOfParameters = 0;

	private boolean translateEmptyBodyAsReturn = false;

	private List<String> localVariables = new LinkedList<String>();

	private List<ByteCodeOperation> operations = new LinkedList<ByteCodeOperation>();

	private Stack<Object> stack = new Stack<Object>();

	private TranslationResultProcessor resultProcessor = null;

	public MethodBlockTranslator(String name, int numberOfParameters, TranslationResultProcessor resultProcessor) {
		this(name, numberOfParameters, resultProcessor, false);
	}

	public MethodBlockTranslator(String name, int numberOfParameters, TranslationResultProcessor resultProcessor,
			boolean translateEmptyBodyAsReturn) {
		super();
		this.name = name;
		this.numberOfParameters = numberOfParameters;
		this.resultProcessor = resultProcessor;
		this.translateEmptyBodyAsReturn = translateEmptyBodyAsReturn;
	}

	@Override
	public void visitCode() {
		translatorLogger.debug("Translating code for '" + name + "'");
	}

	@Override
	public void visitEnd() {

		for (ByteCodeOperation byteCodeOperation : operations) {
			byteCodeOperation.execute(numberOfParameters, stack, localVariables);
		}

		SourceElement result = null;
		try {
			Object last = stack.pop();
			if (last instanceof IExpression) {
				IExpression expression = (IExpression) last;
				result = new ExpressionStatement(expression);
			}
			else if (last instanceof Statement) {
				result = (Statement) last;
			}
			else {
				result = null;
			}
		}
		catch (EmptyStackException e) {
			result = null;
		}

		if (result == null && translateEmptyBodyAsReturn) {
			result = new ReturnStatement();
		}

		List<String> parameters = new LinkedList<String>();

		if (numberOfParameters > 0) {
			for (int i = numberOfParameters; i < localVariables.size(); i++) {
				parameters.add(localVariables.get(i));
			}
		}

		translatorLogger.debug("Translated block: " + result);

		resultProcessor.processResult(parameters, result);
	}

	@Override
	public void visitFieldInsn(int opcode, String owner, String name, String desc) {

		switch (opcode) {
		case Opcodes.GETFIELD:
			operations.add(new GetField(name));
			break;
		case Opcodes.PUTFIELD:
			operations.add(new PutField(name));
			break;
		default:
			translatorLogger.warn("Missing bytecode instruction code: " + opcode);
			break;
		}
	}

	@Override
	public void visitInsn(int opcode) {

		ByteCodeOperation operation = CONSTANT_BYTECODE_OPERATIONS.get(opcode);
		if (operation != null) {
			operations.add(operation);
		}
		else {
			translatorLogger.warn("Missing bytecode instruction code: " + opcode);
		}
	}

	@Override
	public void visitMethodInsn(int opcode, String owner, String name, String desc) {

		switch (opcode) {
		case Opcodes.INVOKEVIRTUAL:
			operations.add(new InvokeVirtual(name));
			break;
		case Opcodes.INVOKESTATIC:
			operations.add(new InvokeStatic());
			break;
		case Opcodes.INVOKESPECIAL:
			Type[] args = Type.getArgumentTypes(desc);
			operations.add(new InvokeSpecial(owner, args));
			break;
		default:
			translatorLogger.warn("Missing bytecode instruction code: " + opcode);
			break;
		}

	}

	@Override
	public void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index) {
		translatorLogger.debug("Local variable: '" + name + "' with index " + index);
		localVariables.add(index, name);
	}

	@Override
	public void visitVarInsn(int opcode, int var) {

		switch (opcode) {
		case Opcodes.ALOAD:
			operations.add(new ALoadOperation(var));
			break;
		case Opcodes.ASTORE:
			operations.add(new AStoreOperation(var));
			break;
		default:
			translatorLogger.warn("Missing local variable instruction code: " + opcode + " with variable index " + var);
			break;
		}

	}

	@Override
	public void visitLdcInsn(Object constant) {
		operations.add(new ConstantLoadOperation(constant, "LDC"));
	}

	@Override
	public void visitTypeInsn(int opcode, String desc) {
		switch (opcode) {
		case Opcodes.NEW:
			operations.add(new CreateObjectOperation(desc));
			break;

		default:
			translatorLogger.warn("Missing type instruction code: " + opcode);
			break;
		}
	}

}

class ALoadOperation implements ByteCodeOperation {

	protected static final Log translatorLogger = LogFactory.getLog("Translator");

	private int index = -1;

	public ALoadOperation(int index) {
		this.index = index;
	}

	public void execute(int numberOfParameters, Stack<Object> stack, List<String> localVariables) {
		translatorLogger.info("ALOAD: push object reference in local variable " + index + " onto the stack");
		stack.add(new Identifier(localVariables.get(index)));
	}

	@Override
	public String toString() {
		return "aload " + index;
	}

}